<html>
<head>
  <meta charset="UTF-8" />
  <meta
    name="viewport"
    content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
  />
  <meta http-equiv="X-UA-Compatible" content="ie=edge" />
  <title>proxyVue</title>
  <style>
    #app {
      margin: 100px auto 0 auto;
      width: 300px;
    }
    #btn {
      margin: 10px auto;
    }
  </style>
</head>
<body>
<div id="app">
  <input type="text" v-model="num" />
  <input id="btn" type="button" value="添加到Todolist" @click="addList" /><br/>
  <div>
    <span>您输入的是：</span>
    <span v-bind="num"></span>
  </div>
  <div>
    <div>num:{{num}}nnn:{{num}}</div>
  </div>
  <div>
    {{  num  }}
  </div>
  <div>name:{{obj.name}}</div>
  <ul id="list">
  </ul>
</div>
</body>

<script>
const reg = /{{.*?}}/g
const getVal = (data, exp) => {
  let arr = exp.split('.')
  let val = data
  arr.forEach(key => {
    val = val[key]
  })
  return val
}
const replaceText = (val, data) => {
  return val.replace(reg, (word) =>{
    return getVal(data, word.slice(2,-2).trim())
  })
}
class proxyVue {
  constructor(options) {
    this.$options = options || {};
    this.$methods = this._methods = this.$options.methods;
    this.$watch = this._watch = this.$options.watch;
    this.$mounted = this.$options.mounted;
    const data = (this._data = this.$options.data);
    this.subscribe = {};
    this.observe(data);
    this.compile(options.el);
    this.$mounted()
  }
  publish(watcher) {
    if (!this.subscribe[watcher.property])
      this.subscribe[watcher.property] = [];
    this.subscribe[watcher.property].push(watcher);
  }
  observe(data) {
    // Vue实例化编译过程将模板中data的属性调用发布（publish）至订阅者（subscribe）进行收集，data属性作为观察者（observe）通过属性的监听来通知订阅者进行更新dom
    // 订阅者与观察者之前通过data属性关联触发
    const that = this;
    let handler = {
      get(target, property) {
        return target[property];
      },
      set(target, property, value) {
        console.log('property:', property)
        const oldVal = target[property]
        let res = Reflect.set(target, property, value);
        that.subscribe[property] && that.subscribe[property].map(item => {
          item.update();
        });
        const wh = that.$watch[property]
        if (wh) {
          if (typeof wh === 'function') {
            wh(value, oldVal)
          } else {
            wh.handler(value, oldVal)
          }
        }
        if (typeof target[property] === 'object') {
          target[property] = new Proxy(target[property], handler)
        }
        return res;
      }
    };
    this.$data = new Proxy(data, handler);
  }
  compile(el) {
    const nd = typeof el === 'string' ? document.querySelector(el).children : el.children
    const nodes = Array.prototype.slice.call(
      nd
    );
    let data = this.$data;
    nodes.map(node => {
      if (node.children.length > 0) this.compile(node);
      if (node.hasAttribute("v-bind")) {
        let property = node.getAttribute("v-bind");
        this.publish(new Watcher(node, "innerHTML", data, property));
      }
      if (node.hasAttribute("v-model")) {
        let property = node.getAttribute("v-model");
        node.value = getVal(data, property)
        this.publish(new Watcher(node, "value", data, property));
        node.addEventListener("input", () => {
          data[property] = node.value;
        });
      }
      if (node.hasAttribute("@click")) {
        let methodName = node.getAttribute("@click");
        let mothod = this.$methods[methodName].bind(data);
        node.addEventListener("click", mothod);
      }
      Array.from(node.childNodes).forEach(childNode => {
        if (childNode.nodeType === 3 && reg.test(childNode.nodeValue)) {
          childNode.nodeValue.match(reg).map(im =>{
            this.publish(new Watcher(childNode, "text", data, im.slice(2,-2).trim(), childNode.nodeValue));
          })
          childNode.nodeValue = replaceText(childNode.nodeValue, data)
        }
      })
    });
  }
}
class Watcher {
  constructor(node, attr, data, property, nodeValue) {
    this.node = node;
    this.attr = attr;
    this.data = data;
    this.property = property;
    this.nodeValue = nodeValue;
  }
  update() {
    if (this.attr !== 'text') {
      this.node[this.attr] = getVal(this.data, this.property)
    } else {// 文本节点
      this.node.nodeValue = replaceText(this.nodeValue, this.data)
    }
  }
}
// 渲染todolist列表
const Render = {
  // 初始化
  init: function(arr) {
    const fragment = document.createDocumentFragment();
    for (let i = 0; i < arr.length; i++) {
      const li = document.createElement("li");
      li.textContent = arr[i];
      fragment.appendChild(li);
    }
    document.getElementById('list').appendChild(fragment);
  },
  addList: function(val) {
    const li = document.createElement("li");
    li.textContent = val;
    document.getElementById('list').appendChild(li);
  }
};
// 实例化一个proxyVue
window.onload = function() {
  let vm = new proxyVue({
    el: "#app",
    data: {
      num: '0',
      arr: [],
      obj: {
        name: 'xiao'
      }
    },
    mounted () {
      console.log('mounted')
    },
    methods: {
      addList() {
        this.arr.push(this.num)
        // this.arr = [...this.arr, this.num]
        Render.addList(this.num);
      }
    },
    watch: {
      num (val, oldVal) {
        console.log(val,',', oldVal)
      },
      arr: {
        handler (val, oldVal) {
          console.log(val,',', oldVal)
        }
      }
    }
  });
};
</script>
</html>
